package cn.calm.mvc;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.handler.MappedInterceptor;
import org.springframework.web.util.UrlPathHelper;

@Service
public class OsgiHandlerMapping extends AbstractHandlerMapping implements
		ServiceListener {
	private UrlPathHelper urlPathHelper = new UrlPathHelper();

	private PathMatcher pathMatcher = new AntPathMatcher();

	private Object rootHandler;

	private boolean lazyInitHandlers = false;

	private final Map<String, Object> handlerMap = new LinkedHashMap<String, Object>();
	private final Map<Class<?>, RequestMapping> cachedMappings = new HashMap<Class<?>, RequestMapping>();
	private MappedInterceptors mappedInterceptors;
	private BundleContext bundleContext;

	/**
	 * Set if URL lookup should always use the full path within the current
	 * servlet context. Else, the path within the current servlet mapping is
	 * used if applicable (that is, in the case of a ".../*" servlet mapping in
	 * web.xml).
	 * <p>
	 * Default is "false".
	 * 
	 * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
	 */
	public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
		this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
	}

	/**
	 * Set if context path and request URI should be URL-decoded. Both are
	 * returned <i>undecoded</i> by the Servlet API, in contrast to the servlet
	 * path.
	 * <p>
	 * Uses either the request encoding or the default encoding according to the
	 * Servlet spec (ISO-8859-1).
	 * 
	 * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
	 */
	public void setUrlDecode(boolean urlDecode) {
		this.urlPathHelper.setUrlDecode(urlDecode);
	}

	/**
	 * Set the UrlPathHelper to use for resolution of lookup paths.
	 * <p>
	 * Use this to override the default UrlPathHelper with a custom subclass, or
	 * to share common UrlPathHelper settings across multiple HandlerMappings
	 * and MethodNameResolvers.
	 * 
	 * @see org.springframework.web.servlet.mvc.multiaction.AbstractUrlMethodNameResolver#setUrlPathHelper
	 */
	public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
		Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
		this.urlPathHelper = urlPathHelper;
	}

	/**
	 * Set the PathMatcher implementation to use for matching URL paths against
	 * registered URL patterns. Default is AntPathMatcher.
	 * 
	 * @see org.springframework.util.AntPathMatcher
	 */
	public void setPathMatcher(PathMatcher pathMatcher) {
		Assert.notNull(pathMatcher, "PathMatcher must not be null");
		this.pathMatcher = pathMatcher;
	}

	/**
	 * Return the PathMatcher implementation to use for matching URL paths
	 * against registered URL patterns.
	 */
	public PathMatcher getPathMatcher() {
		return this.pathMatcher;
	}

	/**
	 * Set the root handler for this handler mapping, that is, the handler to be
	 * registered for the root path ("/").
	 * <p>
	 * Default is <code>null</code>, indicating no root handler.
	 */
	public void setRootHandler(Object rootHandler) {
		this.rootHandler = rootHandler;
	}

	/**
	 * Return the root handler for this handler mapping (registered for "/"), or
	 * <code>null</code> if none.
	 */
	public Object getRootHandler() {
		return this.rootHandler;
	}

	/**
	 * Set whether to lazily initialize handlers. Only applicable to singleton
	 * handlers, as prototypes are always lazily initialized. Default is
	 * "false", as eager initialization allows for more efficiency through
	 * referencing the controller objects directly.
	 * <p>
	 * If you want to allow your controllers to be lazily initialized, make them
	 * "lazy-init" and set this flag to true. Just making them "lazy-init" will
	 * not work, as they are initialized through the references from the handler
	 * mapping in this case.
	 */
	public void setLazyInitHandlers(boolean lazyInitHandlers) {
		this.lazyInitHandlers = lazyInitHandlers;
	}

	public void setMappedInterceptors(MappedInterceptor[] mappedInterceptors) {
		this.mappedInterceptors = new MappedInterceptors(mappedInterceptors);
	}

	@Override
	protected void initInterceptors() {
		super.initInterceptors();
		Map<String, MappedInterceptor> mappedInterceptors = BeanFactoryUtils
				.beansOfTypeIncludingAncestors(getApplicationContext(),
						MappedInterceptor.class, true, false);
		if (!mappedInterceptors.isEmpty()) {
			this.mappedInterceptors = new MappedInterceptors(mappedInterceptors
					.values().toArray(
							new MappedInterceptor[mappedInterceptors.size()]));
		}

	}

	/**
	 * Look up a handler for the URL path of the given request.
	 * 
	 * @param request
	 *            current HTTP request
	 * @return the handler instance, or <code>null</code> if none found
	 */
	@Override
	protected Object getHandlerInternal(HttpServletRequest request)
			throws Exception {
		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
		Object handler = lookupHandler(lookupPath, request);
		if (handler == null) {
			// We need to care for the default handler directly, since we need
			// to
			// expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
			Object rawHandler = null;
			if ("/".equals(lookupPath)) {
				rawHandler = getRootHandler();
			}
			if (rawHandler == null) {
				rawHandler = getDefaultHandler();
			}
			if (rawHandler != null) {
				// Bean name or resolved handler?
				if (rawHandler instanceof String) {
					String handlerName = (String) rawHandler;
					rawHandler = getApplicationContext().getBean(handlerName);
				}
				validateHandler(rawHandler, request);
				handler = buildPathExposingHandler(rawHandler, lookupPath,
						lookupPath, null);
			}
		}
		if (handler != null && this.mappedInterceptors != null) {
			Set<HandlerInterceptor> mappedInterceptors = this.mappedInterceptors
					.getInterceptors(lookupPath, this.pathMatcher);
			if (!mappedInterceptors.isEmpty()) {
				HandlerExecutionChain chain;
				if (handler instanceof HandlerExecutionChain) {
					chain = (HandlerExecutionChain) handler;
				} else {
					chain = new HandlerExecutionChain(handler);
				}
				chain.addInterceptors(mappedInterceptors
						.toArray(new HandlerInterceptor[mappedInterceptors
								.size()]));
			}
		}
		if (handler != null && logger.isDebugEnabled()) {
			logger.debug("Mapping [" + lookupPath + "] to handler '" + handler
					+ "'");
		} else if (handler == null && logger.isTraceEnabled()) {
			logger.trace("No handler mapping found for [" + lookupPath + "]");
		}
		return handler;
	}

	/**
	 * Look up a handler instance for the given URL path.
	 * <p>
	 * Supports direct matches, e.g. a registered "/test" matches "/test", and
	 * various Ant-style pattern matches, e.g. a registered "/t*" matches both
	 * "/test" and "/team". For details, see the AntPathMatcher class.
	 * <p>
	 * Looks for the most exact pattern, where most exact is defined as the
	 * longest path pattern.
	 * 
	 * @param urlPath
	 *            URL the bean is mapped to
	 * @param request
	 *            current HTTP request (to expose the path within the mapping
	 *            to)
	 * @return the associated handler instance, or <code>null</code> if not
	 *         found
	 * @see #exposePathWithinMapping
	 * @see org.springframework.util.AntPathMatcher
	 */
	protected Object lookupHandler(String urlPath, HttpServletRequest request)
			throws Exception {
		// Direct match?
		Object handler = this.handlerMap.get(urlPath);
		if (handler != null) {
			// Bean name or resolved handler?
			if (handler instanceof String) {
				String handlerName = (String) handler;
				handler = getApplicationContext().getBean(handlerName);
			}
			validateHandler(handler, request);
			return buildPathExposingHandler(handler, urlPath, urlPath, null);
		}
		// Pattern match?
		List<String> matchingPatterns = new ArrayList<String>();
		for (String registeredPattern : this.handlerMap.keySet()) {
			if (getPathMatcher().match(registeredPattern, urlPath)) {
				matchingPatterns.add(registeredPattern);
			}
		}
		String bestPatternMatch = null;
		if (!matchingPatterns.isEmpty()) {
			Collections.sort(matchingPatterns, getPathMatcher()
					.getPatternComparator(urlPath));
			if (logger.isDebugEnabled()) {
				logger.debug("Matching patterns for request [" + urlPath
						+ "] are " + matchingPatterns);
			}
			bestPatternMatch = matchingPatterns.get(0);
		}
		if (bestPatternMatch != null) {
			handler = this.handlerMap.get(bestPatternMatch);
			// Bean name or resolved handler?
			if (handler instanceof String) {
				String handlerName = (String) handler;
				handler = getApplicationContext().getBean(handlerName);
			}
			validateHandler(handler, request);
			String pathWithinMapping = getPathMatcher()
					.extractPathWithinPattern(bestPatternMatch, urlPath);
			Map<String, String> uriTemplateVariables = getPathMatcher()
					.extractUriTemplateVariables(bestPatternMatch, urlPath);
			return buildPathExposingHandler(handler, bestPatternMatch,
					pathWithinMapping, uriTemplateVariables);
		}
		// No handler found...
		return null;
	}

	/**
	 * Validate the given handler against the current request.
	 * <p>
	 * The default implementation is empty. Can be overridden in subclasses, for
	 * example to enforce specific preconditions expressed in URL mappings.
	 * 
	 * @param handler
	 *            the handler object to validate
	 * @param request
	 *            current HTTP request
	 * @throws Exception
	 *             if validation failed
	 */
	protected void validateHandler(Object handler, HttpServletRequest request)
			throws Exception {
	}

	/**
	 * Build a handler object for the given raw handler, exposing the actual
	 * handler, the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}, as well as
	 * the {@link #URI_TEMPLATE_VARIABLES_ATTRIBUTE} before executing the
	 * handler.
	 * <p>
	 * The default implementation builds a {@link HandlerExecutionChain} with a
	 * special interceptor that exposes the path attribute and uri template
	 * variables
	 * 
	 * @param rawHandler
	 *            the raw handler to expose
	 * @param pathWithinMapping
	 *            the path to expose before executing the handler
	 * @param uriTemplateVariables
	 *            the URI template variables, can be <code>null</code> if no
	 *            variables found
	 * @return the final handler object
	 */
	protected Object buildPathExposingHandler(Object rawHandler,
			String bestMatchingPattern, String pathWithinMapping,
			Map<String, String> uriTemplateVariables) {

		HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler);
		chain.addInterceptor(new PathExposingHandlerInterceptor(
				bestMatchingPattern, pathWithinMapping));
		if (!CollectionUtils.isEmpty(uriTemplateVariables)) {
			chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(
					uriTemplateVariables));
		}
		return chain;
	}

	/**
	 * Expose the path within the current mapping as request attribute.
	 * 
	 * @param pathWithinMapping
	 *            the path within the current mapping
	 * @param request
	 *            the request to expose the path to
	 * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
	 */
	protected void exposePathWithinMapping(String bestMatchingPattern,
			String pathWithinMapping, HttpServletRequest request) {
		request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE,
				bestMatchingPattern);
		request.setAttribute(
				HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE,
				pathWithinMapping);
	}

	/**
	 * Expose the URI templates variables as request attribute.
	 * 
	 * @param uriTemplateVariables
	 *            the URI template variables
	 * @param request
	 *            the request to expose the path to
	 * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
	 */
	protected void exposeUriTemplateVariables(
			Map<String, String> uriTemplateVariables, HttpServletRequest request) {
		request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
				uriTemplateVariables);
	}

	/**
	 * Register the specified handler for the given URL paths.
	 * 
	 * @param urlPaths
	 *            the URLs that the bean should be mapped to
	 * @param beanName
	 *            the name of the handler bean
	 * @throws BeansException
	 *             if the handler couldn't be registered
	 * @throws IllegalStateException
	 *             if there is a conflicting handler registered
	 */
	protected void registerHandler(String[] urlPaths, String beanName)
			throws BeansException, IllegalStateException {
		Assert.notNull(urlPaths, "URL path array must not be null");
		for (String urlPath : urlPaths) {
			registerHandler(urlPath, beanName);
		}
	}

	/**
	 * Register the specified handler for the given URL path.
	 * 
	 * @param urlPath
	 *            the URL the bean should be mapped to
	 * @param handler
	 *            the handler instance or handler bean name String (a bean name
	 *            will automatically be resolved into the corresponding handler
	 *            bean)
	 * @throws BeansException
	 *             if the handler couldn't be registered
	 * @throws IllegalStateException
	 *             if there is a conflicting handler registered
	 */
	protected void registerHandler(String urlPath, Object handler)
			throws BeansException, IllegalStateException {
		Assert.notNull(urlPath, "URL path must not be null");
		Assert.notNull(handler, "Handler object must not be null");
		Object resolvedHandler = handler;

		// Eagerly resolve handler if referencing singleton via name.
		if (!this.lazyInitHandlers && handler instanceof String) {
			String handlerName = (String) handler;
			if (getApplicationContext().isSingleton(handlerName)) {
				resolvedHandler = getApplicationContext().getBean(handlerName);
			}
		}

		Object mappedHandler = this.handlerMap.get(urlPath);
		if (mappedHandler != null) {
			if (mappedHandler != resolvedHandler) {
				throw new IllegalStateException("Cannot map handler ["
						+ handler + "] to URL path [" + urlPath
						+ "]: There is already handler [" + resolvedHandler
						+ "] mapped.");
			}
		} else {
			if (urlPath.equals("/")) {
				if (logger.isInfoEnabled()) {
					logger.info("Root mapping to handler [" + resolvedHandler
							+ "]");
				}
				setRootHandler(resolvedHandler);
			} else if (urlPath.equals("/*")) {
				if (logger.isInfoEnabled()) {
					logger.info("Default mapping to handler ["
							+ resolvedHandler + "]");
				}
				setDefaultHandler(resolvedHandler);
			} else {
				this.handlerMap.put(urlPath, resolvedHandler);
				if (logger.isInfoEnabled()) {
					logger.info("Mapped URL path [" + urlPath
							+ "] onto handler [" + resolvedHandler + "]");
				}
			}
		}
	}

	/**
	 * Return the registered handlers as an unmodifiable Map, with the
	 * registered path as key and the handler object (or handler bean name in
	 * case of a lazy-init handler) as value.
	 * 
	 * @see #getDefaultHandler()
	 */
	public final Map<String, Object> getHandlerMap() {
		return Collections.unmodifiableMap(this.handlerMap);
	}

	/**
	 * Special interceptor for exposing the
	 * {@link AbstractUrlHandlerMapping#PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}
	 * attribute.
	 * 
	 * @link AbstractUrlHandlerMapping#exposePathWithinMapping
	 */
	private class PathExposingHandlerInterceptor extends
			HandlerInterceptorAdapter {

		private final String bestMatchingPattern;

		private final String pathWithinMapping;

		private PathExposingHandlerInterceptor(String bestMatchingPattern,
				String pathWithinMapping) {
			this.bestMatchingPattern = bestMatchingPattern;
			this.pathWithinMapping = pathWithinMapping;
		}

		@Override
		public boolean preHandle(HttpServletRequest request,
				HttpServletResponse response, Object handler) {
			exposePathWithinMapping(this.bestMatchingPattern,
					this.pathWithinMapping, request);
			request.setAttribute(HandlerMapping.INTROSPECT_TYPE_LEVEL_MAPPING, true);
			return true;
		}
	}

	/**
	 * Special interceptor for exposing the
	 * {@link AbstractUrlHandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE}
	 * attribute.
	 * 
	 * @link AbstractUrlHandlerMapping#exposePathWithinMapping
	 */
	private class UriTemplateVariablesHandlerInterceptor extends
			HandlerInterceptorAdapter {

		private final Map<String, String> uriTemplateVariables;

		private UriTemplateVariablesHandlerInterceptor(
				Map<String, String> uriTemplateVariables) {
			this.uriTemplateVariables = uriTemplateVariables;
		}

		@Override
		public boolean preHandle(HttpServletRequest request,
				HttpServletResponse response, Object handler) {
			exposeUriTemplateVariables(this.uriTemplateVariables, request);
			return true;
		}
	}

	/**
	 * Checks for presence of the
	 * {@link org.springframework.web.bind.annotation.RequestMapping} annotation
	 * on the handler class and on any of its methods.
	 */
	protected String[] determineUrlsForHandler(Object handler) {
		Class<?> handlerType = handler.getClass();
		RequestMapping mapping = AnnotationUtils.findAnnotation(handlerType,
				RequestMapping.class);
		if (mapping != null) {
			// @RequestMapping found at type level
			this.cachedMappings.put(handlerType, mapping);
			Set<String> urls = new LinkedHashSet<String>();
			String[] typeLevelPatterns = mapping.value();
			if (typeLevelPatterns.length > 0) {
				// @RequestMapping specifies paths at type level
				String[] methodLevelPatterns = determineUrlsForHandlerMethods(
						handlerType, true);
				for (String typeLevelPattern : typeLevelPatterns) {
					if (!typeLevelPattern.startsWith("/")) {
						typeLevelPattern = "/" + typeLevelPattern;
					}
					boolean hasEmptyMethodLevelMappings = false;
					for (String methodLevelPattern : methodLevelPatterns) {
						if (methodLevelPattern == null) {
							hasEmptyMethodLevelMappings = true;
						} else {
							String combinedPattern = getPathMatcher().combine(
									typeLevelPattern, methodLevelPattern);
							addUrlsForPath(urls, combinedPattern);
						}
					}
					if (hasEmptyMethodLevelMappings
							|| org.springframework.web.servlet.mvc.Controller.class
									.isAssignableFrom(handlerType)) {
						addUrlsForPath(urls, typeLevelPattern);
					}
				}
				return StringUtils.toStringArray(urls);
			} else {
				// actual paths specified by @RequestMapping at method level
				return determineUrlsForHandlerMethods(handlerType, false);
			}
		} else if (AnnotationUtils
				.findAnnotation(handlerType, Controller.class) != null) {
			// @RequestMapping to be introspected at method level
			return determineUrlsForHandlerMethods(handlerType, false);
		} else {
			return null;
		}
	}

	/**
	 * Derive URL mappings from the handler's method-level mappings.
	 * 
	 * @param handlerType
	 *            the handler type to introspect
	 * @param hasTypeLevelMapping
	 *            whether the method-level mappings are nested within a
	 *            type-level mapping
	 * @return the array of mapped URLs
	 */
	protected String[] determineUrlsForHandlerMethods(Class<?> handlerType,
			final boolean hasTypeLevelMapping) {
		String[] subclassResult = determineUrlsForHandlerMethods(handlerType);
		if (subclassResult != null) {
			return subclassResult;
		}

		final Set<String> urls = new LinkedHashSet<String>();
		Class<?>[] handlerTypes = Proxy.isProxyClass(handlerType) ? handlerType
				.getInterfaces() : new Class<?>[] { handlerType };
		for (Class<?> currentHandlerType : handlerTypes) {
			ReflectionUtils.doWithMethods(currentHandlerType,
					new ReflectionUtils.MethodCallback() {
						public void doWith(Method method) {
							RequestMapping mapping = AnnotationUtils
									.findAnnotation(method,
											RequestMapping.class);
							if (mapping != null) {
								String[] mappedPatterns = mapping.value();
								if (mappedPatterns.length > 0) {
									for (String mappedPattern : mappedPatterns) {
										if (!hasTypeLevelMapping
												&& !mappedPattern
														.startsWith("/")) {
											mappedPattern = "/" + mappedPattern;
										}
										addUrlsForPath(urls, mappedPattern);
									}
								} else if (hasTypeLevelMapping) {
									// empty method-level RequestMapping
									urls.add(null);
								}
							}
						}
					}, ReflectionUtils.NON_BRIDGED_METHODS);
		}
		return StringUtils.toStringArray(urls);
	}

	/**
	 * Add URLs and/or URL patterns for the given path.
	 * 
	 * @param urls
	 *            the Set of URLs for the current bean
	 * @param path
	 *            the currently introspected path
	 */
	protected void addUrlsForPath(Set<String> urls, String path) {
		urls.add(path);
		if (this.useDefaultSuffixPattern && path.indexOf('.') == -1
				&& !path.endsWith("/")) {
			urls.add(path + ".*");
			urls.add(path + "/");
		}
	}

	/**
	 * Derive URL mappings from the handler's method-level mappings.
	 * 
	 * @param handlerType
	 *            the handler type to introspect
	 * @return the array of mapped URLs
	 */
	protected String[] determineUrlsForHandlerMethods(Class<?> handlerType) {
		return null;
	}

	private boolean useDefaultSuffixPattern = true;

	@Override
	public void serviceChanged(ServiceEvent event) {
		int type = event.getType();
		if (type == ServiceEvent.REGISTERED) {
			ServiceReference<?> reference = event.getServiceReference();
			Object service = bundleContext.getService(reference);
			String[] urls = determineUrlsForHandler(service);
			if (urls != null) {
				for (String u : urls) {
					registerHandler(u, service);
				}
			}
		} else if (type == ServiceEvent.UNREGISTERING) {
			ServiceReference<?> reference = event.getServiceReference();
			Object service = bundleContext.getService(reference);
			String[] urls = determineUrlsForHandler(service);
			if (urls != null) {
				for (String u : urls) {
					handlerMap.remove(u);
				}
			}
		}
	}

	public BundleContext getBundleContext() {
		return bundleContext;
	}

	public void setBundleContext(BundleContext bundleContext) {
		this.bundleContext = bundleContext;
	}
}
